package com.xiashitech.agent.db.extload;

import cn.hutool.core.util.HashUtil;
import cn.hutool.core.util.StrUtil;
import com.xiashitech.agent.config.StartConfig;
import com.xiashitech.agent.proxy.hutool.FileUtil;
import com.xiashitech.agent.proxy.hutool.Setting;
import com.xiashitech.agent.proxy.hutool.extloadproxy.PooledDSFactoryProxy;
import com.xiashitech.agent.utils.XSUtil;

import javax.sql.DataSource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;

public class LocalDataSource {

    public static String agentInstanceId = null;
    public static String agentInstanceIdOrigin = null;
    private static DataSource dataSource = null;
    private static final String SCHEMA_NAME = "xs-data";
    private static final String DEFAULT_DATA_DIR = "~/agent-db";

    static {
        try {
            String systemId = StartConfig.getParamValueFromKey(StartConfig.systemId);
            if (XSUtil.isEmpty(systemId)){
                throw new IllegalArgumentException("systemId must not be empty");
            }
            String serviceId = System.getProperty("otel.service.name", systemId);
            // 为了配置转义方便，将 xs.dataDir 改为 xs.datadir
            //xs.datadir 最好是映射到容器外的一个存放路径。不同宿主机部署同一个systemId的节点时，最好将数据文件存放路径映射
            String dataDir = System.getProperty("xs.datadir", System.getProperty ( "user.home" ) + "/agent-db");// DEFAULT_DATA_DIR);
            String schemaBasePath = StrUtil.format("{}/{}/{}/", dataDir, systemId, serviceId);
            System.out.println("lock fileName is, schemaBasePath: " + schemaBasePath);
            String schemaPath = null;
            //迭代Schema编号：算法逻辑变量。
            for (int i = 1; i <= 1000; i++) {
                schemaPath =  schemaBasePath + i + "/" ;
                boolean locked = false;
                if (FileUtil.exist(schemaPath)){
                    // 有这个编号的数据库，可以尝试连接（如果空闲，则能连接）
                    String fileName = schemaPath + SCHEMA_NAME + ".lock.db";
                    if (FileUtil.exist(fileName)){
                        // 数据文件被占用，放弃连接
                        // DO Nothing

                        // System.out.println("lock fileName is: " + fileName);
                        // ArrayList<String> readUtf8Lines = FileUtil.readLines(path,CharsetUtil.UTF_8, new ArrayList<>());
                        // for(String readUtf8Line :readUtf8Lines){
                        //     System.out.println(readUtf8Line);
                        // }
                        // read file into stream, try-with-resources
                        // try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
                        //     Optional<String> stringOptional = stream.filter(line->line.startsWith("server=")).findFirst();
                        //     if(stringOptional.isPresent()) {
                        //         String uri = stringOptional.get();
                        //         System.out.println("lock fileName useful line: " + uri);
                        //         String[] target = uri.split(":");
                        //         // InetAddress theAddress = InetAddress.getByName(target[0].replaceAll("\\\\",""));
                        //         try{
                        //             // Socket socket = new Socket(target[0].replaceAll("\\\\",""), Integer.parseInt(target[1]));
                        //             Socket socket = new Socket("127.0.0.1", Integer.parseInt(target[1]));
                        //             System.out.println("lock fileName connect: " + uri);
                        //             socket.close();
                        //             locked = true; // 不报异常表示能连上，即端口被占用
                        //         } catch (Exception ee) {
                        //             System.out.println("lock fileName connect exception: " + ee.getMessage());
                        //         }
                        //     }
                        // } catch (IOException e) {
                        //     e.printStackTrace();
                        //     System.out.println("lock fileName readfile exception: " + e.getMessage());
                        // }
                        // 通过对锁文件修改是否成功来确认当前锁文件是否被用
                        try{
                            File file = new File(fileName);
                            FileOutputStream fos = new FileOutputStream (file,true ) ;
                            String updContext = "\n# xs.update.test\n" ;
                            fos.write(updContext.getBytes()) ;
                            fos.close ();
                            Thread.sleep(10000); // TODO 等待 10s，以便文件能够修改回去，10s 足够，是否太长、多长合适不确定
                            try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
                                Optional<String> stringOptional = stream.filter(line->{
                                    // System.out.println("lock fileName line is: " + line);
                                    return line.startsWith(updContext.trim());
                                }).findFirst();
                                if(!stringOptional.isPresent()) {
                                    locked = true; // 新添加的行不存在，表示锁文件被占用
                                }
                            } catch (Exception ee) {

                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    if(!locked) {
                        // 数据文件空闲，可以连接
                        // TODO(maybe) 可能由于这个数据文件一段时间没有被使用，已经缺失一些增量数据了。启动时需要将本地增量max(pushId) 发送给 AgentServer 比对增量版本差异。 或者依赖其它判断机制，比如如果判断出数据文件已经 X 天没被使用，则直接触发一次全量更新。
                        createDataSource(schemaPath);
                        XSUtil.info("ReConnected to old data-file: {}", schemaPath);
                    }
                } else {
                    // 没有这个编号的数据库，创建新的数据文件并建立连接，并触发一次全量数据推送
                    createDataSource(schemaPath);
                    XSUtil.info("Connected to new data-file :{}", schemaPath);
                }
                if (Objects.nonNull(dataSource)) break;
            }
            generateAgentInstanceId(systemId, schemaPath);
        }catch (Throwable throwable){
            XSUtil.error("Create dataSource fail. {}", throwable.getMessage());
            StartConfig.throwableStackPrinter(throwable);
        }
    }

    private static void createDataSource(String schemaPath){
        // IFEXISTS=TRUE; 只连接不会自动创建数据文件
        // AUTO_SERVER=TRUE; FOR DEV. 追加这个配置项仅用于本地开发调试.
        Setting settingInstance = Setting.getInstance("xs.agent.db/agent-db.setting");
        settingInstance.set("url", StrUtil.format("jdbc:h2:{}/{};AUTO_RECONNECT=TRUE;AUTO_SERVER=TRUE;", schemaPath, SCHEMA_NAME));
        settingInstance.set("maxActive", "1000");
        // 在 class path 自动嗅探 DataSource 组件
        // dataSource = DSFactory.create(setting).getDataSource();
        // 实例化特定的 DatsSource 组件，避免潜在冲突和干扰
        dataSource = (DataSource)PooledDSFactoryProxy.getInstance(settingInstance.getValue()).getDataSource();
    }

    // 附加业务：生成一个相对固定的唯一标识当前启动 Agent 的字符串。（Agent 实例ID）供gRPC推送通道使用。
    // 算法描述：HASH(系统ID(启动参数的systemId) + 服务名称(启动参数serviceName) + 根据数据源算法选中的当前数据文件Path)
    // 算法缺陷：上述算法仅仅适配单主机启动多个docker，将数据文件目录映射到宿主机的情况，一个系统模块部署到多个主机的情况并不能有效兼容。
    // 缺陷规避：方法1：docker 映射数据文件目录的时候加以区分，不同的主机映射不同的目录。【建议采用：高效、短平快】
    // 缺陷规避：方法2：docker 统一映射到一个不同主机都可以访问的网络共享目录。【不建议，可能破坏客户原有部署结构，且映射后数据文件读写性能不明】
    // 缺陷规避：方法3：动态获取 docker 宿主机特征值(IP、hostname 等)。【不建议，客户部署架构复杂多变，容易踩坑，暂不实现】
    private static void generateAgentInstanceId(String systemId, String dbPath){
        // 如果没设置就使用 systemId, 当前版本这个 serviceName 在权限业务里没有实际意义。
        String serviceName = System.getProperty("otel.service.name", systemId);
        agentInstanceIdOrigin = systemId + "|" + serviceName + "|" + dbPath;
        agentInstanceId = String.valueOf(HashUtil.bkdrHash(agentInstanceIdOrigin));
        XSUtil.info("Generated agent instance id[systemId | serviceName | dbPath]: {} by: {}", agentInstanceId, agentInstanceIdOrigin);
    }

    public static DataSource getDS(){
        return dataSource;
    }

}
